﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using GammaJul.ReSharper.GammaXaml.Psi.Xaml;
using JetBrains.Annotations;
using JetBrains.ReSharper.Feature.Services.CSharp.Generate;
using JetBrains.ReSharper.Feature.Services.Generate;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CSharp;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.CodeAnnotations;
using JetBrains.ReSharper.Psi.Util;
using JetBrains.ReSharper.Psi.Xaml.Impl.Resolve;
using JetBrains.Util;

namespace GammaJul.ReSharper.GammaXaml.Services.CSharp.Generate {

	/// <summary>
	/// Generates dependency property CLR wrappers.
	/// </summary>
	[GeneratorBuilder(GeneratorStandardKinds.Properties, typeof(CSharpLanguage))]
	[GeneratorBuilder(GeneratorStandardKinds.ReadProperties, typeof(CSharpLanguage))]
	internal sealed class DpPropertiesBuilder : GeneratorBuilderBase<CSharpGeneratorContext> {
		private const string OwnerParamName = "owner";
		private const string ValueParamName = "value";

		protected override bool HasProcessableElements(CSharpGeneratorContext context, IEnumerable<IGeneratorElement> elements) {
			return elements.OfType<DpGeneratorField>().Any();
		}

		[NotNull]
		private static IList<IGeneratorOption> GetOptions(CSharpGeneratorContext context, bool includeVirtual) {
			int readOnlyChoiceIndex = context.Kind == GeneratorStandardKinds.ReadProperties ? 1 : 0;
			var list = new List<IGeneratorOption> {
				new GeneratorOptionSelector(CSharpBuilderOptions.AccessRights, "Access &Rights", CSharpBuilderOptions.AccessRightsChoices[0], CSharpBuilderOptions.AccessRightsChoices),
				new GeneratorOptionSelector(CSharpBuilderOptions.ReadOnly, "Read-&only", CSharpBuilderOptions.ReadOnlyChoices[readOnlyChoiceIndex], CSharpBuilderOptions.ReadOnlyChoices),
				new GeneratorOptionBoolean(CSharpBuilderOptions.DebuggerStepsThrough, "&Debugger steps through", false) { Persist = true }
			};

			if (includeVirtual && !context.ClassDeclaration.IsSealed)
				list.Add(new GeneratorOptionBoolean(CSharpBuilderOptions.Virtual, "&Virtual", false));

			return list;
		}

		protected override IList<IGeneratorOption> GetGlobalOptions(CSharpGeneratorContext context) {
			return GetOptions(context, true);
		}

		protected override IList<IGeneratorOption> GetInputElementOptions(IGeneratorElement inputElement, CSharpGeneratorContext context) {
			var dpGeneratorField = inputElement as DpGeneratorField;
			return dpGeneratorField != null
				? GetOptions(context, !dpGeneratorField.IsAttached)
				: base.GetInputElementOptions(inputElement, context);
		}

		protected override void Process(CSharpGeneratorContext context) {
			CSharpElementFactory elementFactory = CSharpElementFactory.GetInstance(context.PsiModule);
			IXamlTypeCache xamlTypeCache = XamlTypeCache.GetCache(context.PsiModule);
			ITypeElement debuggerStepThrough = TypeElementUtil.GetTypeElementByClrName(PredefinedType.DEBUGGER_STEP_THROUGH_ATTRIBUTE_CLASS, context.PsiModule);
			PredefinedType predefinedType = context.PsiModule.GetPredefinedType();
			ITypeElement notNullType = context.PsiModule.GetPsiServices().GetCodeAnnotationsCache().GetNullableAttributeTypeForElement(context.Root, CodeAnnotationNullableValue.NOT_NULL);

			foreach (DpGeneratorField generatorField in context.InputElements.OfType<DpGeneratorField>()) {
				if (generatorField.IsAttached)
					GenerateAttachedAccessors(generatorField, context, elementFactory, xamlTypeCache, debuggerStepThrough, predefinedType, notNullType);
				else
					GenerateProperty(generatorField, context, elementFactory, xamlTypeCache, debuggerStepThrough);
			}
		}

		/// <summary>
		/// Generates a property declaration wrapping a dependency property field.
		/// </summary>
		/// <param name="generatorField">Information about the field to wrap, as a <see cref="DpGeneratorField"/>.</param>
		/// <param name="context">The generator context.</param>
		/// <param name="elementFactory">A C# element factory.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <param name="debuggerStepThrough">A type element representing <see cref="DebuggerStepThroughAttribute"/>.</param>
		private static void GenerateProperty([NotNull] DpGeneratorField generatorField, [NotNull] CSharpGeneratorContext context,
			[NotNull] CSharpElementFactory elementFactory, [NotNull] IXamlTypeCache xamlTypeCache, [NotNull] ITypeElement debuggerStepThrough) {

			IField dpField = generatorField.DeclaredElement;
			string dpName = dpField.GetExpectedDpName(xamlTypeCache);
			bool useStepThrough = Boolean.Parse(context.GetElementOptionValue(generatorField, CSharpBuilderOptions.DebuggerStepsThrough));

			IPropertyDeclaration declaration = elementFactory.CreatePropertyDeclaration(generatorField.DpRegisteredType, dpName);

			// generate getter
			IAccessorDeclaration getter = declaration.AddAccessorDeclarationAfter(elementFactory.CreateAccessorDeclaration(AccessorKind.GETTER, true), null);
			if (useStepThrough)
				getter.AddAttributeAfter(elementFactory.CreateAttribute(debuggerStepThrough), null);
			ICSharpStatement statement = generatorField.DpRegisteredType.IsObject()
				? elementFactory.CreateStatement("return GetValue($0);", dpField)
				: elementFactory.CreateStatement("return ($0) GetValue($1);", generatorField.DpRegisteredType, dpField);
			getter.Body.AddStatementAfter(statement, null);

			// generate setter if needed
			IField dpKeyField = dpField.GetContainingType().GetDpKeyField(dpName, xamlTypeCache);
			if (!IsReadOnly(context.GetElementOptionValue(generatorField, CSharpBuilderOptions.ReadOnly), dpKeyField != null)) {
				IAccessorDeclaration setter = declaration.AddAccessorDeclarationAfter(elementFactory.CreateAccessorDeclaration(AccessorKind.SETTER, true), getter);
				if (useStepThrough)
					setter.AddAttributeAfter(elementFactory.CreateAttribute(debuggerStepThrough), null);
				statement = elementFactory.CreateStatement("SetValue($0, value);", dpKeyField ?? dpField);
				setter.Body.AddStatementAfter(statement, null);
			}

			PutMemberDeclaration(generatorField, declaration, context);
		}

		/// <summary>
		/// Generates methods wrapping an attached dependency property field.
		/// </summary>
		/// <param name="generatorField">Information about the field to wrap, as a <see cref="DpGeneratorField"/>.</param>
		/// <param name="context">The generator context.</param>
		/// <param name="elementFactory">A C# element factory.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <param name="debuggerStepThrough">A type element representing <see cref="DebuggerStepThroughAttribute"/>.</param>
		/// <param name="predefinedType">A <see cref="PredefinedType"/> instance.</param>
		/// <param name="notNullTypeElement">The type element representing <c>NotNullAttribute</c>.</param>
		private static void GenerateAttachedAccessors([NotNull] DpGeneratorField generatorField, [NotNull] CSharpGeneratorContext context,
			[NotNull] CSharpElementFactory elementFactory, [NotNull] IXamlTypeCache xamlTypeCache, [NotNull] ITypeElement debuggerStepThrough,
			[NotNull] PredefinedType predefinedType, [CanBeNull] ITypeElement notNullTypeElement) {

			IField dpField = generatorField.DeclaredElement;
			string dpName = dpField.GetExpectedDpName(xamlTypeCache);
			Assertion.Assert(dpName != null, "dpName != null");
			ITypeElement effectiveStepThrough = Boolean.Parse(context.GetElementOptionValue(generatorField, CSharpBuilderOptions.DebuggerStepsThrough))
				? debuggerStepThrough
				: null;
			
			// generate getter
			var getter = (IMethodDeclaration) elementFactory.CreateTypeMemberDeclaration(
				"$0 $1($2 $3);",
				generatorField.DpRegisteredType,
				dpName.ToDpAttachedGetterName(),
				xamlTypeCache.DependencyObject,
				OwnerParamName);

			ICSharpStatement valueStatement = generatorField.DpRegisteredType.IsObject()
				 ? elementFactory.CreateStatement("return $0.GetValue($1);", OwnerParamName, dpField)
			     : elementFactory.CreateStatement("return ($0) $1.GetValue($2);", generatorField.DpRegisteredType, OwnerParamName, dpField);
			SetAttachedAccessorBody(getter, elementFactory, effectiveStepThrough, predefinedType, notNullTypeElement, valueStatement);
			PutMemberDeclaration(generatorField, getter, context);

			// generate setter if needed
			IField dpKeyField = dpField.GetContainingType().GetDpKeyField(dpName, xamlTypeCache);
			if (IsReadOnly(context.GetElementOptionValue(generatorField, CSharpBuilderOptions.ReadOnly), dpKeyField != null))
				return;

			var setter = (IMethodDeclaration) elementFactory.CreateTypeMemberDeclaration(
				"void $0($1 $2, $3 $4)",
				dpName.ToDpAttachedSetterName(),
				xamlTypeCache.DependencyObject,
				OwnerParamName,
				generatorField.DpRegisteredType,
				ValueParamName);

			valueStatement = elementFactory.CreateStatement("$0.SetValue($1, $2);", OwnerParamName, dpKeyField ?? dpField, ValueParamName);
			SetAttachedAccessorBody(setter, elementFactory, effectiveStepThrough, predefinedType, notNullTypeElement, valueStatement);
			PutMemberDeclaration(generatorField, setter, context);
		}

		private static void SetAttachedAccessorBody([NotNull] IMethodDeclaration declaration, [NotNull] CSharpElementFactory elementFactory,
			[CanBeNull] ITypeElement debuggerStepThrough, [NotNull] PredefinedType predefinedType, [CanBeNull] ITypeElement notNullTypeElement,
			[NotNull] ICSharpStatement valueStatement) {

			declaration.SetStatic(true);
			declaration.SetBody(elementFactory.CreateEmptyBlock());
			if (debuggerStepThrough != null)
				declaration.AddAttributeAfter(elementFactory.CreateAttribute(debuggerStepThrough), null);
			if (notNullTypeElement != null) {
				var attributesOwnerDeclaration = declaration.ParameterDeclarationsEnumerable.First() as IAttributesOwnerDeclaration;
				if (attributesOwnerDeclaration != null)
					attributesOwnerDeclaration.AddAttributeAfter(elementFactory.CreateAttribute(notNullTypeElement), null);
			}

			ICSharpStatement throwStatement = declaration.Body.AddStatementAfter(
				elementFactory.CreateStatement(
					"if ($0 == null) throw new $1(\"$0\");",
					OwnerParamName,
					predefinedType.ArgumentNullException),
				null);
			
			declaration.Body.AddStatementAfter(valueStatement, throwStatement);
		}

		private static void PutMemberDeclaration([NotNull] DpGeneratorField generatorField, [NotNull] IClassMemberDeclaration declaration, [NotNull] CSharpGeneratorContext context) {
			
			declaration.SetAccessRights(CSharpBuilderOptions.GetAccessRights(context.GetElementOptionValue(generatorField, CSharpBuilderOptions.AccessRights)));
			if (!declaration.IsStatic)
				declaration.SetVirtual(Boolean.Parse(context.GetElementOptionValue(generatorField, CSharpBuilderOptions.Virtual)));
			
			DpGeneratorField fieldClosure = generatorField;
			context.PutMemberDeclaration(declaration, generatorField, decl => new GeneratorDpDeclarationElement(decl, fieldClosure));
		}

		private static bool IsReadOnly(string readOnlyString, bool hasDpKey) {
			if (readOnlyString == CSharpBuilderOptions.ReadOnlyChoices[0] /* Automatic */)
				return hasDpKey;
			if (readOnlyString == CSharpBuilderOptions.ReadOnlyChoices[1] /* Yes */)
				return true;
			return false;
		}

		public override double Priority {
			get { return -10.0; }
		}

	}

}